1 module test_allocator;
2 
3 // tracks allocations and throws in the destructor if there is a memory leak
4 // it also throws when there is an attempt to deallocate memory that wasn't
5 // allocated
6 struct TestAllocator {
7     import std.experimental.allocator.common: platformAlignment;
8     import std.experimental.allocator.mallocator: Mallocator;
9 
10     alias allocator = Mallocator.instance;
11 
12     @safe @nogc nothrow:
13 
14     private static struct ByteRange {
15         void* ptr;
16         size_t length;
17         inout(void)[] opSlice() @trusted @nogc nothrow pure inout {
18             return ptr[0 .. length];
19         }
20     }
21 
22     private ByteRange[] _allocations;
23     private int _numAllocations;
24     private char[1024] _textBuffer;
25 
26     enum uint alignment = platformAlignment;
27 
28     void[] allocate(size_t numBytes) scope {
29         import std.experimental.allocator: makeArray, expandArray;
30 
31         ++_numAllocations;
32 
33         auto ret = allocator.allocate(numBytes);
34         if(ret.length == 0) return ret;
35 
36         auto newEntry = ByteRange(&ret[0], ret.length);
37 
38         if(_allocations is null)
39             _allocations = allocator.makeArray(1, newEntry);
40         else
41             () @trusted { allocator.expandArray(_allocations, 1, newEntry); }();
42 
43         return ret;
44     }
45 
46     bool deallocate(void[] bytes) scope pure {
47         import std.algorithm: remove, canFind;
48         static if (__VERSION__ < 2077)
49         {
50             import core.stdc.stdio: sprintf;
51             alias pureSprintf = sprintf;
52         }
53 
54         bool pred(ByteRange other) { return other.ptr == bytes.ptr && other.length == bytes.length; }
55 
56         static char[1024] buffer;
57 
58         // @trusted because this is `scope` and we're taking the address of it
59         assert(() @trusted { return &this !is null; }(), "Attempting to deallocate when `this` is null");
60 
61         if(!_allocations.canFind!pred) {
62             auto index = pureSprintf(
63                 () @trusted { return _textBuffer.ptr; }(),
64                 "Cannot deallocate unknown byte range.\nPtr: %p, length: %ld, allocations:\n",
65                 () @trusted { return bytes.ptr; }(), bytes.length);
66             index = printAllocations(_textBuffer, index);
67             _textBuffer[index] = 0;
68             debug
69                 assert(false, _textBuffer[0 .. index].dup);
70             else
71                 assert(false, "Cannot deallocate unknown byte range. Use debug mode to see more information");
72         }
73 
74         _allocations = _allocations.remove!pred;
75 
76         return () @trusted { return allocator.deallocate(bytes); }();
77     }
78 
79     bool deallocateAll() scope pure {
80         foreach(ref allocation; _allocations) {
81             deallocate(allocation[]);
82         }
83         return true;
84     }
85 
86     auto numAllocations() pure const scope {
87         return _numAllocations;
88     }
89 
90     ~this() pure {
91         verify;
92         finalise;
93     }
94 
95     private void finalise() scope pure {
96         import std.experimental.allocator: dispose;
97         deallocateAll;
98         () @trusted { allocator.dispose(_allocations); }();
99     }
100 
101     void verify() scope pure {
102         static if (__VERSION__ < 2077)
103         {
104             import core.stdc.stdio: sprintf;
105             alias pureSprintf = sprintf;
106         }
107 
108         if(_allocations.length) {
109             auto index = pureSprintf(
110                 () @trusted { return _textBuffer.ptr; }(),
111                 "Memory leak in TestAllocator. Allocations:\n");
112             index = printAllocations(_textBuffer, index);
113             _textBuffer[index] = 0;
114 
115             finalise;  // avoid asan leaks
116 
117             debug
118                 assert(false, _textBuffer[0 .. index].dup);
119             else
120                 assert(false, "Memory leak in TestAllocator. Use debug mode to see more information");
121         }
122     }
123 
124     int printAllocations(int N)(ref char[N] buffer, int index = 0) pure const scope {
125         static if (__VERSION__ < 2077)
126         {
127             import core.stdc.stdio: sprintf;
128             alias pureSprintf = sprintf;
129         }
130 
131         index += pureSprintf(&buffer[index], "[");
132 
133         if(_allocations !is null) {
134             foreach(ref allocation; _allocations) {
135                 index += pureSprintf(&buffer[index], "ByteRange(%p, %ld), ",
136                                      allocation.ptr, allocation.length);
137             }
138         }
139 
140         index += pureSprintf(&buffer[index], "]");
141         return index;
142     }
143 }
144 
145 static if (__VERSION__ >= 2077)
146 {
147     /* Private bits that allow sprintf to become pure */
148     private int pureSprintf(A...)(scope char* s, scope const(char*) format, A va)
149         @trusted pure nothrow
150     {
151         const errnosave = fakePureErrno();
152         const ret = fakePureSprintf(s, format, va);
153         fakePureErrno() = errnosave;
154 
155         return ret;
156     }
157 
158     extern (C) private @system @nogc nothrow
159     {
160         version(DigitalMars) {
161             ref int fakePureErrnoImpl()
162             {
163                 import core.stdc.errno;
164                 return errno();
165             }
166         } else
167               ref int fakePureErrnoImpl();
168     }
169 
170     extern (C) private pure @system @nogc nothrow
171     {
172         pragma(mangle, "fakePureErrnoImpl") ref int fakePureErrno();
173         pragma(mangle, "sprintf") int fakePureSprintf(scope char* s, scope const(char*) format, ...);
174     }
175 }
176 
177 @safe @nogc nothrow unittest {
178     import std.experimental.allocator : allocatorObject;
179     import std.experimental.allocator.building_blocks.stats_collector;
180     import std.experimental.allocator.mallocator: Mallocator;
181     import std.conv : to;
182 
183     alias SCAlloc = StatsCollector!(TestAllocator, Options.bytesUsed);
184 
185     SCAlloc allocator;
186     auto buf = allocator.allocate(10);
187     allocator.deallocate(buf);
188     assert(allocator.bytesUsed == 0);
189 }
190 
191 
192 @safe @nogc nothrow unittest {
193     auto obj = TestAllocator();
194     scope ptr = &obj;
195 }
196 
197 
198 @safe @nogc unittest {
199     import std.experimental.allocator: makeArray, expandArray, dispose;
200     auto allocator = TestAllocator();
201     auto array = allocator.makeArray!int(3);
202     // expandArray is @system because Mallocator.reallocate is @system,
203     // and that in turn is because reallocate may make pointers dangle.
204     const expanded = () @trusted { return allocator.expandArray(array, 2); }();
205     allocator.dispose(array);
206 }